// @ts-check

import { Card } from "../common/Card.js";
import { getCardColors } from "../common/color.js";
import { I18n } from "../common/I18n.js";
import { clampValue, lowercaseTrim } from "../common/ops.js";
import { createProgressNode, flexLayout } from "../common/render.js";
import { wakatimeCardLocales } from "../translations.js";

/** Import language colors.
 *
 * @description Here we use the workaround found in
 * https://stackoverflow.com/questions/66726365/how-should-i-import-json-in-node
 * since vercel is using v16.14.0 which does not yet support json imports without the
 * --experimental-json-modules flag.
 */
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const languageColors = require("../common/languageColors.json"); // now works

const DEFAULT_CARD_WIDTH = 495;
const MIN_CARD_WIDTH = 250;
const COMPACT_LAYOUT_MIN_WIDTH = 400;
const DEFAULT_LINE_HEIGHT = 25;
const PROGRESSBAR_PADDING = 130;
const HIDDEN_PROGRESSBAR_PADDING = 170;
const COMPACT_LAYOUT_PROGRESSBAR_PADDING = 25;
const TOTAL_TEXT_WIDTH = 275;

/**
 * Creates the no coding activity SVG node.
 *
 * @param {object} props The function properties.
 * @param {string} props.color No coding activity text color.
 * @param {string} props.text No coding activity translated text.
 * @returns {string} No coding activity SVG node string.
 */
const noCodingActivityNode = ({ color, text }) => {
  return `
    <text x="25" y="11" class="stat bold" fill="${color}">${text}</text>
  `;
};

/**
 * @typedef {import('../fetchers/types').WakaTimeLang} WakaTimeLang
 */

/**
 * Format language value.
 *
 * @param {Object} args The function arguments.
 * @param {WakaTimeLang} args.lang The language object.
 * @param {"time" | "percent"} args.display_format The display format of the language node.
 * @returns {string} The formatted language value.
 */
const formatLanguageValue = ({ display_format, lang }) => {
  return display_format === "percent"
    ? `${lang.percent.toFixed(2).toString()} %`
    : lang.text;
};

/**
 * Create compact WakaTime layout.
 *
 * @param {Object} args The function arguments.
 * @param {WakaTimeLang} args.lang The languages array.
 * @param {number} args.x The x position of the language node.
 * @param {number} args.y The y position of the language node.
 * @param {"time" | "percent"} args.display_format The display format of the language node.
 * @returns {string} The compact layout language SVG node.
 */
const createCompactLangNode = ({ lang, x, y, display_format }) => {
  // @ts-ignore
  const color = languageColors[lang.name] || "#858585";
  const value = formatLanguageValue({ display_format, lang });

  return `
    <g transform="translate(${x}, ${y})">
      <circle cx="5" cy="6" r="5" fill="${color}" />
      <text data-testid="lang-name" x="15" y="10" class='lang-name'>
        ${lang.name} - ${value}
      </text>
    </g>
  `;
};

/**
 * Create WakaTime language text node item.
 *
 * @param {Object} args The function arguments.
 * @param {WakaTimeLang[]} args.langs The language objects.
 * @param {number} args.y The y position of the language node.
 * @param {"time" | "percent"} args.display_format The display format of the language node.
 * @param {number} args.card_width Width in px of the card.
 * @returns {string[]} The language text node items.
 */
const createLanguageTextNode = ({ langs, y, display_format, card_width }) => {
  const LEFT_X = 25;
  const RIGHT_X_BASE = 230;
  const rightOffset = (card_width - DEFAULT_CARD_WIDTH) / 2;
  const RIGHT_X = RIGHT_X_BASE + rightOffset;

  return langs.map((lang, index) => {
    const isLeft = index % 2 === 0;
    return createCompactLangNode({
      lang,
      x: isLeft ? LEFT_X : RIGHT_X,
      y: y + DEFAULT_LINE_HEIGHT * Math.floor(index / 2),
      display_format,
    });
  });
};

/**
 * Create WakaTime text item.
 *
 * @param {Object} args The function arguments.
 * @param {string} args.id The id of the text node item.
 * @param {string} args.label The label of the text node item.
 * @param {string} args.value The value of the text node item.
 * @param {number} args.index The index of the text node item.
 * @param {number} args.percent Percentage of the text node item.
 * @param {boolean=} args.hideProgress Whether to hide the progress bar.
 * @param {string} args.progressBarColor The color of the progress bar.
 * @param {string} args.progressBarBackgroundColor The color of the progress bar background.
 * @param {number} args.progressBarWidth The width of the progress bar.
 * @returns {string} The text SVG node.
 */
const createTextNode = ({
  id,
  label,
  value,
  index,
  percent,
  hideProgress,
  progressBarColor,
  progressBarBackgroundColor,
  progressBarWidth,
}) => {
  const staggerDelay = (index + 3) * 150;
  const cardProgress = hideProgress
    ? null
    : createProgressNode({
        x: 110,
        y: 4,
        progress: percent,
        color: progressBarColor,
        width: progressBarWidth,
        // @ts-ignore
        name: label,
        progressBarBackgroundColor,
        delay: staggerDelay + 300,
      });

  return `
    <g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
      <text class="stat bold" y="12.5" data-testid="${id}">${label}:</text>
      <text
        class="stat"
        x="${hideProgress ? HIDDEN_PROGRESSBAR_PADDING : PROGRESSBAR_PADDING + progressBarWidth}"
        y="12.5"
      >${value}</text>
      ${cardProgress}
    </g>
  `;
};

/**
 * Recalculating percentages so that, compact layout's progress bar does not break when
 * hiding languages.
 *
 * @param {WakaTimeLang[]} languages The languages array.
 * @returns {void} The recalculated languages array.
 */
const recalculatePercentages = (languages) => {
  const totalSum = languages.reduce(
    (totalSum, language) => totalSum + language.percent,
    0,
  );
  const weight = +(100 / totalSum).toFixed(2);
  languages.forEach((language) => {
    language.percent = +(language.percent * weight).toFixed(2);
  });
};

/**
 * Retrieves CSS styles for a card.
 *
 * @param {Object} colors The colors to use for the card.
 * @param {string} colors.titleColor The title color.
 * @param {string} colors.textColor The text color.
 * @returns {string} Card CSS styles.
 */
const getStyles = ({
  // eslint-disable-next-line no-unused-vars
  titleColor,
  textColor,
}) => {
  return `
    .stat {
      font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
    }
    @supports(-moz-appearance: auto) {
      /* Selector detects Firefox */
      .stat { font-size:12px; }
    }
    .stagger {
      opacity: 0;
      animation: fadeInAnimation 0.3s ease-in-out forwards;
    }
    .not_bold { font-weight: 400 }
    .bold { font-weight: 700 }
  `;
};

/**
 * Normalize incoming width (string or number) and clamp to minimum.
 *
 * @param {Object} args The function arguments.
 * @param {WakaTimeOptions["layout"] | undefined} args.layout The incoming layout value.
 * @param {number|undefined} args.value The incoming width value.
 * @returns {number} The normalized width value.
 */
const normalizeCardWidth = ({ value, layout }) => {
  if (value === undefined || value === null || isNaN(value)) {
    return DEFAULT_CARD_WIDTH;
  }
  return Math.max(
    layout === "compact" ? COMPACT_LAYOUT_MIN_WIDTH : MIN_CARD_WIDTH,
    value,
  );
};

/**
 * @typedef {import('../fetchers/types').WakaTimeData} WakaTimeData
 * @typedef {import('./types').WakaTimeOptions} WakaTimeOptions
 */

/**
 * Renders WakaTime card.
 *
 * @param {Partial<WakaTimeData>} stats WakaTime stats.
 * @param {Partial<WakaTimeOptions>} options Card options.
 * @returns {string} WakaTime card SVG.
 */
const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
  let { languages = [] } = stats;
  const {
    hide_title = false,
    hide_border = false,
    card_width,
    hide,
    line_height = DEFAULT_LINE_HEIGHT,
    title_color,
    icon_color,
    text_color,
    bg_color,
    theme = "default",
    hide_progress,
    custom_title,
    locale,
    layout,
    langs_count = languages.length,
    border_radius,
    border_color,
    display_format = "time",
    disable_animations,
  } = options;

  const normalizedWidth = normalizeCardWidth({ value: card_width, layout });

  const shouldHideLangs = Array.isArray(hide) && hide.length > 0;
  if (shouldHideLangs) {
    const languagesToHide = new Set(hide.map((lang) => lowercaseTrim(lang)));
    languages = languages.filter(
      (lang) => !languagesToHide.has(lowercaseTrim(lang.name)),
    );
  }

  // Since the percentages are sorted in descending order, we can just
  // slice from the beginning without sorting.
  languages = languages.slice(0, langs_count);
  recalculatePercentages(languages);

  const i18n = new I18n({
    locale,
    translations: wakatimeCardLocales,
  });

  const lheight = parseInt(String(line_height), 10);

  const langsCount = clampValue(langs_count, 1, langs_count);

  // returns theme based colors with proper overrides and defaults
  const { titleColor, textColor, iconColor, bgColor, borderColor } =
    getCardColors({
      title_color,
      icon_color,
      text_color,
      bg_color,
      border_color,
      theme,
    });

  const filteredLanguages = languages
    .filter((language) => language.hours || language.minutes)
    .slice(0, langsCount);

  // Calculate the card height depending on how many items there are
  // but if rank circle is visible clamp the minimum height to `150`
  let height = Math.max(45 + (filteredLanguages.length + 1) * lheight, 150);

  const cssStyles = getStyles({
    titleColor,
    textColor,
  });

  let finalLayout = "";

  // RENDER COMPACT LAYOUT
  if (layout === "compact") {
    const width = normalizedWidth - 5;
    height =
      90 + Math.round(filteredLanguages.length / 2) * DEFAULT_LINE_HEIGHT;

    // progressOffset holds the previous language's width and used to offset the next language
    // so that we can stack them one after another, like this: [--][----][---]
    let progressOffset = 0;
    const compactProgressBar = filteredLanguages
      .map((language) => {
        const progress =
          ((width - COMPACT_LAYOUT_PROGRESSBAR_PADDING) * language.percent) /
          100;

        // @ts-ignore
        const languageColor = languageColors[language.name] || "#858585";

        const output = `
          <rect
            mask="url(#rect-mask)"
            data-testid="lang-progress"
            x="${progressOffset}"
            y="0"
            width="${progress}"
            height="8"
            fill="${languageColor}"
          />
        `;
        progressOffset += progress;
        return output;
      })
      .join("");

    finalLayout = `
      <mask id="rect-mask">
      <rect x="${COMPACT_LAYOUT_PROGRESSBAR_PADDING}" y="0" width="${width - 2 * COMPACT_LAYOUT_PROGRESSBAR_PADDING}" height="8" fill="white" rx="5" />
      </mask>
      ${compactProgressBar}
      ${
        filteredLanguages.length
          ? createLanguageTextNode({
              y: 25,
              langs: filteredLanguages,
              display_format,
              card_width: normalizedWidth,
            }).join("")
          : noCodingActivityNode({
              // @ts-ignore
              color: textColor,
              text: stats.is_coding_activity_visible
                ? stats.is_other_usage_visible
                  ? i18n.t("wakatimecard.nocodingactivity")
                  : i18n.t("wakatimecard.nocodedetails")
                : i18n.t("wakatimecard.notpublic"),
            })
      }
    `;
  } else {
    finalLayout = flexLayout({
      items: filteredLanguages.length
        ? filteredLanguages.map((language, index) => {
            return createTextNode({
              id: language.name,
              label: language.name,
              value: formatLanguageValue({ display_format, lang: language }),
              index,
              percent: language.percent,
              // @ts-ignore
              progressBarColor: titleColor,
              // @ts-ignore
              progressBarBackgroundColor: textColor,
              hideProgress: hide_progress,
              progressBarWidth: normalizedWidth - TOTAL_TEXT_WIDTH,
            });
          })
        : [
            noCodingActivityNode({
              // @ts-ignore
              color: textColor,
              text: stats.is_coding_activity_visible
                ? stats.is_other_usage_visible
                  ? i18n.t("wakatimecard.nocodingactivity")
                  : i18n.t("wakatimecard.nocodedetails")
                : i18n.t("wakatimecard.notpublic"),
            }),
          ],
      gap: lheight,
      direction: "column",
    }).join("");
  }

  // Get title range text
  let titleText = i18n.t("wakatimecard.title");
  switch (stats.range) {
    case "last_7_days":
      titleText += ` (${i18n.t("wakatimecard.last7days")})`;
      break;
    case "last_year":
      titleText += ` (${i18n.t("wakatimecard.lastyear")})`;
      break;
  }

  const card = new Card({
    customTitle: custom_title,
    defaultTitle: titleText,
    width: normalizedWidth,
    height,
    border_radius,
    colors: {
      titleColor,
      textColor,
      iconColor,
      bgColor,
      borderColor,
    },
  });

  if (disable_animations) {
    card.disableAnimations();
  }

  card.setHideBorder(hide_border);
  card.setHideTitle(hide_title);
  card.setCSS(
    `
    ${cssStyles}
    @keyframes slideInAnimation {
      from {
        width: 0;
      }
      to {
        width: calc(100%-100px);
      }
    }
    @keyframes growWidthAnimation {
      from {
        width: 0;
      }
      to {
        width: 100%;
      }
    }
    .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
    #rect-mask rect{
      animation: slideInAnimation 1s ease-in-out forwards;
    }
    .lang-progress{
      animation: growWidthAnimation 0.6s ease-in-out forwards;
    }
    `,
  );

  return card.render(`
    <svg x="0" y="0" width="100%">
      ${finalLayout}
    </svg>
  `);
};

export { renderWakatimeCard };
export default renderWakatimeCard;
